Skip to content

perf(render_shapes): PathCollection of pre-built Paths (avoid per-shape Patch construction)#735

Merged
timtreis merged 2 commits into
mainfrom
perf/shapes-pathcollection
Jun 21, 2026
Merged

perf(render_shapes): PathCollection of pre-built Paths (avoid per-shape Patch construction)#735
timtreis merged 2 commits into
mainfrom
perf/shapes-pathcollection

Conversation

@timtreis

Copy link
Copy Markdown
Member

Closes #733.

Problem

render_shapes (matplotlib) is dominated by per-shape Patch construction — identical across no-color / categorical / continuous (cProfile, 200k circles, ~23 s): _build_shape_patches builds one mpatches.Circle/Polygon per shape (each Patch.__init__ resolves default colors via to_rgba ~4×/patch + recomputes a transform), then PatchCollection.set_paths converts all N to Paths.

Change

Build matplotlib.path.Path objects directly (the form set_paths bakes internally anyway) and return a PathCollection:

  • Byte-identical (same Paths → same backend); preserves holes (compound paths) and all color/alpha/outline logic.
  • One code path, no signature change.
  • Apply the coordinate-system affine once to the shared paths (fill + outline collections reference the same Path objects), replacing the 4 per-collection get_paths() loops.

Results (end-to-end, 300k circles, matplotlib categorical)

wall peak mem (tracemalloc) peak RSS
main 62.2 s 1337 MB 2822 MB
this PR 35.8 s 250 MB 889 MB
~1.7× ~5.3× ~3.2×

Color-mode-independent (the cost was geometry, not color).

Byte-identity (verified)

main-vs-branch full-canvas RGBA max diff 0 across circles / polygons / multipolygon-with-holes × {categorical, continuous, no-color} × {fill, outline}, identity transform. Plus a guard test that the affine is applied exactly once under a non-identity transform + outline.

Datashader + as_points paths untouched (already optimized).

…e Patch objects (#733)

render_shapes (matplotlib) was dominated by constructing one mpatches.Circle/Polygon
per shape (each Patch.__init__ resolves default colours via to_rgba + recomputes a
transform), then PatchCollection.set_paths converting all N to Paths.

Build matplotlib Path objects directly (the form set_paths bakes anyway) and return a
PathCollection. Same Paths -> byte-identical; preserves holes (compound paths) and all
colour/alpha/outline logic; one code path; no signature change.

Apply the coordinate-system affine once to the shared paths (fill + outline collections
reference the same Path objects) instead of per-collection, dropping the 4 get_paths
loops.

End-to-end (300k circles, mpl categorical): ~1.7x faster, ~5.3x lower peak memory.
Byte-identical verified: main-vs-branch RGBA == 0 across circles/polygons/multipolygon
-with-holes x {categorical, continuous, no-color} x {fill, outline}; plus a guard test
that the affine is applied once under a non-identity transform + outline.
@codecov-commenter

codecov-commenter commented Jun 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.26%. Comparing base (2cc819e) to head (21b5477).

Files with missing lines Patch % Lines
src/spatialdata_plot/pl/_geometry.py 92.59% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #735      +/-   ##
==========================================
- Coverage   79.31%   79.26%   -0.05%     
==========================================
  Files          17       17              
  Lines        4607     4596      -11     
  Branches     1031     1028       -3     
==========================================
- Hits         3654     3643      -11     
  Misses        603      603              
  Partials      350      350              
Files with missing lines Coverage Δ
src/spatialdata_plot/pl/render.py 89.55% <100.00%> (-0.07%) ⬇️
src/spatialdata_plot/pl/_geometry.py 79.72% <92.59%> (-0.46%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- circle path: use mpath.Path.circle((x,y), r) instead of hand-rolling
  unit_circle.vertices*r+center (byte-identical; drops the hoist + comment).
- trim narrating/duplicated comments (build docstring, reuse comment) and correct
  the trans-once comment (the old per-collection transform was redundant work, not a
  visible double-transform — verified main-vs-branch byte-identical).
@timtreis timtreis merged commit 55f4970 into main Jun 21, 2026
7 of 8 checks passed
@timtreis timtreis deleted the perf/shapes-pathcollection branch June 21, 2026 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: render shapes via PathCollection of pre-built Paths (avoid per-shape Patch construction)

2 participants